Skip to content

Stop a thread spawning workers in another project's repo (#109) - #111

Merged
currentbits merged 1 commit into
mainfrom
coder/cross-project-work-a-thread-in-one-proje-d587c3
Aug 16, 2026
Merged

currentbits merged 1 commit into
mainfrom
coder/cross-project-work-a-thread-in-one-proje-d587c3

Conversation

@currentbits

Copy link
Copy Markdown
Owner

Fixes the hole behind #109. The issue asked for cross-project reach; investigation found the workspace already had accidental cross-project reach, with no guard rail. Willem's ruling was that a run should never write to a second repo, so this closes the hole rather than building the feature.

What happened

Shipnest-v2 thread, 2026-08-16 12:48–13:10. An agent working in /Users/willem/code/Shipnest-v2 called thread_fork({threadId: "260be2c1"}) — a thread belonging to Solenta. Two grok workers spawned on git worktrees of the wrong repo. Told off, it repeated the mistake four minutes later with 9fafc689, guessed from the title "changelog and website". Cleanup cost two deleted branches and a removed worktree.

Its own diagnosis, from the transcript, was correct:

"I have no way to see which project a thread belongs to — threads_list returns id, title, provider, status, nothing about the workspace — so every fork I make is a guess, and guessing is exactly what caused this."

Root cause

thread_fork(threadId) takes its source thread as an argument, and electron/orchServer.js has no caller identity at all — one workspace-wide bearer token over stateless HTTP. So the server cannot know who is calling, and the caller must name a thread.

Naming one was necessarily a guess, because the agent is never told its own thread id, and threads_list never reported which project a thread belonged to.

thread_send had the same hole: it started a run on any threadId with no check at all. Guarded here too — one guard, both callers.

Changes

  • threads_list reports the project. Each row gains projectId and projectName — the field whose absence the agent named as the reason it guessed.
  • Every run learns its own identity. New services.selfIdNoteFor, a sibling of the existing planboardNoteFor, appends a CLI-only [Thread] note (never stored in the transcript) at the single prompt finalization point in runner.js, stating the run's own thread id, project and cwd. It rides every dispatch rather than the first turn only, so context compaction and resumed sessions don't lose it. Gated on coder-threads actually being registered — with no thread tools in the run there is nothing to pass the ids to — mirroring planboardNoteFor's GitHub-origin gate.
  • thread_fork and thread_send require a stated projectId and reject a thread outside it, naming both projects in the error. A same-project fork of a sibling thread still works; only the project boundary is blocked.

Because each thread stays bound to exactly one project, the diff/commit/PR/worktree views are untouched — which answers all three of #109's original open questions with "nothing changes".

thread_status is deliberately left unguarded: it is read-only, cannot start anything, and threads_list already exposes every thread's title across projects.

Known ceiling

The assert is a self-declared claim, not proof — it catches a confused caller, not a malicious one. Real caller identity would need per-run bearer tokens mapped to thread ids. Rejected as too expensive for this failure mode: claude and codex take per-run MCP config, but grok and kimi register the server globally (grok mcp add … -s user, ~/.kimi-code/mcp.json), so a per-run token cannot reach them without racy global config rewrites — and the incident's workers were grok. Marked with a ponytail: comment naming the upgrade path.

Testing

1597 tests green across all four suites: core 38, renderer 686, electron 754, memory 120. Zero failures.

New coverage in electron/test/orch-server.test.js, which drives the handlers spawn-free via the exported createToolHandlers:

  • a fork and a send targeting a thread in another project both throw, and nothing is forked or started
  • a same-project fork of a sibling thread still succeeds
  • selfIdNoteFor states the ids, and returns "" while coder-threads is down

Deferred

Read-only reach into another project ("go look at how X does this") is left unbuilt until this fix has been lived with. Cross-project writes stay off the table.

Design doc: docs/superpowers/specs/2026-08-16-cross-project-spawn-guard-design.md

  • PR created by the Claude Code agent

An agent working in Shipnest-v2 called thread_fork with a thread id it
picked off threads_list by title. The id belonged to Solenta, so two grok
workers spawned on git worktrees of the wrong repo. Told off, it repeated
the mistake four minutes later with a second guessed id.

Its own diagnosis was correct: threads_list returns id, title, provider,
status and handoffFrom, and nothing about the workspace, so every fork was
a guess. The orchestrator has no caller identity either — one workspace-wide
bearer token over stateless HTTP — so the server could not tell.

Three changes:

- threads_list reports projectId and projectName per thread.
- Every dispatched prompt carries a [Thread] note stating the run's own
  thread id, project and cwd, so the ids can be passed instead of guessed.
  Gated on coder-threads being registered, like planboardNoteFor's
  GitHub-origin gate.
- thread_fork and thread_send require a stated projectId and reject a
  thread outside it, naming both projects. A same-project fork of a sibling
  thread still works; only the boundary is blocked.

The assert is a self-declared claim, not proof: it catches a confused
caller, not a malicious one. Real caller identity needs per-run bearer
tokens, which grok and kimi cannot receive — they register MCP globally at
user scope. Marked with a ponytail: comment naming that ceiling.

Issue #109 asked for cross-project reach. What existed was accidental
cross-project reach with no guard rail, so this closes the hole instead;
cross-project writes stay off the table and read-only reach is deferred.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@currentbits
currentbits merged commit f272255 into main Aug 16, 2026
1 check passed
@currentbits
currentbits deleted the coder/cross-project-work-a-thread-in-one-proje-d587c3 branch October 3, 2026 16:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant